package com.nulldev.util.graphics.rast2d;

import java.util.Arrays;
import java.util.Map;

import com.nulldev.util.JVM.JVM;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.Arrays.maps.FastMaps;
import com.nulldev.util.graphics.renderIt.renderer.global.rColor;
import com.nulldev.util.graphics.renderIt.renderer.global.rColor.rColor_Type;

public class Pixmap {

	/* Formats */

	public static final int PIXMAP_FMT_RGB444 = 1;
	public static final int PIXMAP_FMT_RGB888 = 2;
	public static final int PIXMAP_FMT_RGB565 = 3;

	public static final int PIXMAP_FMT_RGBA8888 = 10;

	/* Global Flags */

	public static final boolean PIXMAP_USE_FAST_LINE_COPY = true;
	public static final boolean PIXMAP_USE_FAST_CANVAS_CLEAR = true;

	/* */

	private volatile int[/* X */][/* Y */] data;
	private final int format;
	private final int pixmap_width, pixmap_height;

	public static final Map<Integer, String> PIXMAP_PIXEL_FORMATS = FastMaps.fixedUniMap(new Object[][]
		{
				/* Alpha formats (will probably be the slowest) */

				{ PIXMAP_FMT_RGBA8888, "RGBA888" },

				/* Alpha-less formats */

				{ PIXMAP_FMT_RGB888, "RGB888" },
				{ PIXMAP_FMT_RGB565, "RGB565" },
				{ PIXMAP_FMT_RGB444, "RGB444" } });

	public static String getFormat(final int fmt) {
		return PIXMAP_PIXEL_FORMATS.getOrDefault(fmt, "UNKNOWN_FORMAT");
	}

	public static boolean isFormat(final int fmt) {
		return PIXMAP_PIXEL_FORMATS.containsKey(fmt);
	}

	public static String getFastOptions() {
		final StringBuffer buf = new StringBuffer();
		buf.append("FAST_LINE|");
		if (PIXMAP_USE_FAST_LINE_COPY)
			buf.append("FAST_LINE_COPY|");
		if (PIXMAP_USE_FAST_CANVAS_CLEAR)
			buf.append("FAST_CANVAS_CLEAR|");
		return buf.substring(0, buf.length() - 1);
	}

	public Pixmap(final int width, final int height) {
		this(width, height, PIXMAP_FMT_RGB888);
	}

	public Pixmap(final int width, final int height, final int format) {
		if (width <= 0)
			throw new IllegalArgumentException("Invalid width!");
		if (height <= 0)
			throw new IllegalArgumentException("Invalid height!");
		if (!isFormat(format))
			throw new IllegalArgumentException("Invalid pixel format!");
		this.data = new int[width][height];
		this.pixmap_width = width;
		this.pixmap_height = height;
		this.format = format;
		for (int y = 0; y < this.pixmap_height; y++)
			Arrays.fill(this.data[y], Integer.MAX_VALUE);
	}

	public int pixmapFormat() {
		return this.format;
	}

	public int pixmapWidth() {
		return this.pixmap_width;
	}

	public int pixmapHeight() {
		return this.pixmap_height;
	}

	public int[][] getData() {
		return this.data;
	}

	public int pixelAt(final int width, final int height) {
		if (width < 0 || width > this.pixmap_width)
			throw new IllegalArgumentException("pixelAt(): Invalid width!");
		if (height < 0 || height > this.pixmap_height)
			throw new IllegalArgumentException("pixelAt(): Invalid height!");
		return this.data[width][height];
	}

	public rColor colorAt(final int width, final int height) {
		if (width < 0 || width > this.pixmap_width)
			throw new IllegalArgumentException("colorAt(): Invalid width!");
		if (height < 0 || height > this.pixmap_height)
			throw new IllegalArgumentException("colorAt(): Invalid height!");
		switch (this.format) {
			case PIXMAP_FMT_RGB444: {
				return new rColor(rColor_Type.RGB_4BIT, this.data[width][height]);
			}
			case PIXMAP_FMT_RGB565: {
				/* FIXME: RGB565 is actually 6-bits */
				return new rColor(rColor_Type.RGB_4BIT, this.data[width][height]);
			}
			case PIXMAP_FMT_RGB888: {
				return new rColor(rColor_Type.RGB_8BIT, this.data[width][height]);
			}
			case PIXMAP_FMT_RGBA8888: {
				/* FIXME: rColors API sucks and doesn't take pixel-ordering into account */
				return new rColor(rColor_Type.ARGB_8BIT, this.data[width][height]);
			}
			default:
				throw new UnsupportedOperationException("Unknown format: " + this.format);
		}
	}

	private int formatColor(final int _color) {
		switch (this.format) {
			case PIXMAP_FMT_RGB444: {
				if (_color >= 0 && _color <= 4096)
					return _color;
				/* TODO: Add RGB888->RGB444 conversion */
			}
			case PIXMAP_FMT_RGB565: {
				if (_color >= 0 && _color <= 65535)
					return _color;
				/* Assume we're dealing with RGB888 and we need to convert. */
				return ((_color & 0b11111000) << 8) | ((_color & 0b11111100) << 3) | (_color >> 3);
			}
			case PIXMAP_FMT_RGB888:
				/* RGB888 is our native format, pass through the value. */
			case PIXMAP_FMT_RGBA8888:
				/* TODO: RGBA8888 should also work... (in theory) */
				return _color;
			default:
				throw new UnsupportedOperationException("Cannot format color for: " + getFormat(this.format));
		}
	}

	public void clear() {
		this.clear(Integer.MAX_VALUE);
	}

	private static final int BLANK_FB_SIZE = 32;
	private static final int[] BLANK_FB;

	static {
		BLANK_FB = new int[BLANK_FB_SIZE];
		Arrays.fill(BLANK_FB, Integer.MAX_VALUE);
	}

	public void clear(final int _color) {
		final int color = formatColor(_color);
		if (PIXMAP_USE_FAST_CANVAS_CLEAR && _color == Integer.MAX_VALUE) {
			for (int x = 0; x < this.pixmap_width; x++) {
				for (int y = 0; y < this.pixmap_height; y += BLANK_FB_SIZE) {
					System.arraycopy(BLANK_FB, 0, this.data[x], y, BLANK_FB_SIZE);
				}
			}
		} else {
			for (int x = 0; x < this.pixmap_width; x++)
				Arrays.fill(this.data[x], color);
		}
	}

	public void importPixmap(final Pixmap pixmap) {
		Variables.requireNonNull(pixmap, "pixmap");
		System.arraycopy(pixmap.data, 0, this.data, 0, pixmap.data.length);
	}

	public void rect(final int x, final int y, final int width, final int height, final int _color) {
		if (x < 0 || y < 0)
			return;
		if (x >= this.pixmap_width || y >= this.pixmap_height)
			return;
		if (width <= 0 || height <= 0)
			return;
		final int color = formatColor(_color);
		// -----> (top AND bottom)
		if (width == 1 && height == 1) {
			this.data[x][y] = color;
		} else {
			final int limitX = MathUtil.clamp(0, this.pixmap_width - 1, (x + width));
			final int limitY = MathUtil.clamp(0, this.pixmap_height - 1, (y + height));
			final int xEnd = MathUtil.clamp(0, this.pixmap_width - 1, (x + width));
			final int yEnd = MathUtil.clamp(0, this.pixmap_height - 1, (y + height));
			for (int px = x; px <= limitX; px++) {
				this.data[px][y] = color;
				this.data[px][yEnd] = color;
			}
			// | | (sides)
			for (int py = y; py <= limitY; py++) {
				this.data[x][py] = color;
				this.data[xEnd][py] = color;
			}
		}
	}

	public void fill(final int x, final int y, final int width, final int height, final int _color) {
		if (x < 0 || y < 0)
			return;
		if (x >= this.pixmap_width || y >= this.pixmap_height)
			return;
		if (width <= 0 || height <= 0)
			return;
		final int color = formatColor(_color);
		if (width == 1 && height == 1) {
			this.data[x][y] = color;
		} else {
			// -----> (top AND bottom)
			final int limitX = MathUtil.clamp(0, this.pixmap_width - 1, (x + width));
			final int limitY = MathUtil.clamp(0, this.pixmap_height - 1, (y + height));
			final int xEnd = MathUtil.clamp(0, this.pixmap_width - 1, (x + width));
			final int yEnd = MathUtil.clamp(0, this.pixmap_height - 1, (y + height));
			for (int px = x; px <= limitX; px++) {
				this.data[px][y] = color;
				this.data[px][yEnd] = color;
			}
			// | | (sides)
			for (int py = y; py <= limitY; py++) {
				this.data[x][py] = color;
				this.data[xEnd][py] = color;
			}
			// insides
			for (int py = (y + 1); py <= limitY; py++) {
				for (int px = (x + 1); px <= limitX; px++) {
					this.data[px][py] = color;
				}
			}
		}
	}

	public void copy(final int x, final int y, final int width, final int height, final int dx, final int dy) {
		if (x < 0 || y < 0)
			return;
		if (dx < 0 || dy < 0)
			return;
		if (x >= this.pixmap_width || y >= this.pixmap_height)
			return;
		if (dx >= this.pixmap_width || dy >= this.pixmap_height)
			return;
		if (width <= 0 || height <= 0)
			return;
		if (PIXMAP_USE_FAST_LINE_COPY) {
			for (int i = 0; i < height; i++) {
				final int sy = (y + i);
				final int oy = (dy + i);
				System.arraycopy(this.data[sy], x, this.data[oy], dx, width);
			}
		} else {
			// TODO: Implement copy(): SLOW
		}
	}

	public void line(final int x1, final int y1, final int x2, final int y2, final int _color) {
		if (x1 < 0 || y1 < 0)
			return;
		if (x1 >= this.pixmap_width || y1 >= this.pixmap_height)
			return;
		if (x2 < 0 || y2 < 0)
			return;
		if (x2 >= this.pixmap_width || y2 >= this.pixmap_height)
			return;
		if (x1 > x2 || y1 > y2)
			return;
		final int color = formatColor(_color);
		if (y1 == y2 && x1 != x2) { /* line(): FAST_X */
			for (int x = x1; x <= x2; x++)
				this.data[x][y1] = color;
		} else if (x1 == x2 && y1 != y2) { /* line(): FAST_Y */
			for (int y = y1; y <= y2; y++)
				this.data[x1][y] = color;
		} else { /* line(): SLOW_XY */
			int y = y1;
			for (int x = x1; x <= x2; x++) {
				this.data[x][y] = color;
				y++;
			}
		}
	}

	public void dumpState() {
		for (int y = 0; y < this.pixmap_height; y++) {
			for (int x = 0; x < this.pixmap_width; x++) {
				JVM.print(this.data[x][y] + ",");
			}
			JVM.println();
		}
	}

	public static void main(String[] args) {
		final Pixmap px = new Pixmap(128, 128, PIXMAP_FMT_RGB888);
		px.fill(5, 5, 32, 32, 800000);
		px.dumpState();
	}

	public void writeLine(final int x, final int currentY, final int[] rgb) {
		if (x + rgb.length > this.pixmap_width)
			return;
		for (int i = 0; i < rgb.length; i++) {
			this.data[x + i][currentY] = rgb[i];
		}
	}

}
